package com.g0dkar.leet.util.ognl;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import ognl.ClassResolver;
import ognl.DefaultClassResolver;
import ognl.DefaultMemberAccess;
import ognl.ListPropertyAccessor;
import ognl.MemberAccess;
import ognl.Ognl;
import ognl.OgnlContext;
import ognl.OgnlException;
import ognl.OgnlRuntime;
import ognl.TypeConverter;

import com.g0dkar.leet.util.ognl.typeConversion.LeetOGNLTypeConverter;
import com.g0dkar.leet.util.ognl.typeConversion.TypeConverterManager;
import com.g0dkar.leet.util.reflection.ReflectionUtils;

/**
 * This class provides many methods that helps the use of OGNL. It always call OGNL using JHeat's custom classes for
 * Type Conversion (using JHeat's {@link com.g0dkar.leet.util.ognl.typeConversion.TypeConverter type conversion
 * features}) and JHeat's implementation of {@link ListPropertyAccessor} for {@link Set} (providing the same
 * functionality for both Lists and Sets). Please, if you're going to use Sets as ONGL Arrays, read the documentation
 * for {@link LeetOGNLSetPropertyAccessor}.
 * 
 * @author Rafael g0dkar Lins
 * 
 * @see LeetOGNLNullHandler
 * @see LeetOGNLSetPropertyAccessor
 * @see LeetOGNLTypeConverter
 * @see com.g0dkar.leet.util.ognl.typeConversion.TypeConverter
 */
public final class OgnlUtils {
	private static MemberAccess OGNL_MEMBER_ACCESS;
	private static ClassResolver OGNL_CLASS_RESOLVER;
	private static TypeConverter OGNL_TYPE_CONVERTER;
	
	private static OgnlContext OGNL_CONTEXT;
	
	static {
		OGNL_CLASS_RESOLVER = new DefaultClassResolver();
		OGNL_TYPE_CONVERTER = new LeetOGNLTypeConverter();
		OGNL_MEMBER_ACCESS = new DefaultMemberAccess(false);
		
		OGNL_CONTEXT = new OgnlContext(OGNL_CLASS_RESOLVER, OGNL_TYPE_CONVERTER, OGNL_MEMBER_ACCESS);
		
		// XWork NullHandler
		// defaultContext.put(ReflectionContextState.CREATE_NULL_OBJECTS, Boolean.TRUE);
		// OgnlRuntime.setNullHandler(Object.class, new OgnlNullHandlerWrapper(new InstantiatingNullHandler()));
		
		// VRaptor NullHandler
		// OgnlRuntime.setNullHandler(Object.class, new ReflectionBasedNullHandler());
		
		// JHeat NullHandler
		OgnlRuntime.setNullHandler(Object.class, new LeetOGNLNullHandler());
		
		// JHeat Generics Object Property Accessor
		OgnlRuntime.setPropertyAccessor(Object.class, new LeetOGNLObjectPropertyAccessor());
		
		// JHeat Set (Collection) Property Accessor
		OgnlRuntime.setPropertyAccessor(Set.class, new LeetOGNLSetPropertyAccessor());
	}
	
	private OgnlUtils() throws InstantiationException {
		throw new InstantiationException("You cannot create an instance of this class.");
	}
	
	public static OgnlContext getJHeatOGNLContext() {
		return OGNL_CONTEXT;
	}
	
	// private static OgnlContext getModelContext(Object modelObject) {
	// Map<String, Object> map = new HashMap<String, Object>(1);
	// map.put("modelObject", modelObject);
	// return new OgnlContext(classResolver, typeConverter, memberAccess, map);
	// }
	
	/**
	 * Sets a value on a object using OGNL to navigate the object tree, convert types, resolve null, etcetera. If a
	 * exception occurrs, the object won't be altered.
	 * 
	 * @param field
	 *            The navigation path
	 * @param value
	 *            The value to set
	 * @param object
	 *            The object that'll be altered
	 * 
	 * @see #setWithException(String, Object, Object)
	 */
	public static void set(String field, Object value, Object object) {
		try {
			setWithException(field, value, object);
		} catch (OgnlException e) {
			// Yeah... sorry...
			e.printStackTrace();
		}
	}
	
	/**
	 * Sets a value on a object using OGNL to navigate the object tree, convert types, resolve null, etcetera.
	 * 
	 * @param field
	 *            The navigation path
	 * @param value
	 *            The value to set
	 * @param object
	 *            The object that'll be altered
	 * @throws OgnlException
	 *             Something went wrong
	 */
	public static void setWithException(String field, Object value, Object object) throws OgnlException {
		Ognl.setValue(field, OGNL_CONTEXT, object, value);
	}
	
	public static Object get(String field, Object object) {
		try {
			return Ognl.getValue(field, OGNL_CONTEXT, object);
		} catch (OgnlException e) {
			return null;
		}
	}
	
	public static String getString(String field, Object object) {
		Object obj = get(field, object);
		
		if (obj != null) {
			return toString(obj);
		}
		else {
			return null;
		}
	}
	
	/**
	 * Turns any {@link Object} into it's {@link String} representation.
	 * 
	 * @param object
	 *            The Object
	 * @return The String representation of the Object
	 */
	public static String toString(Object object) {
		com.g0dkar.leet.util.ognl.typeConversion.TypeConverter converter = TypeConverterManager.getConverterFor(object.getClass());
		
		try {
			return converter.toString(object, null);
		} catch (Exception e) {
			return null;
		}
	}
	
	/**
	 * Turns any {@link Object} into it's {@link String} representation.
	 * 
	 * @param string
	 *            The Object
	 * @return The String representation of the Object
	 */
	@SuppressWarnings("unchecked")
	public static <T> T toString(String string, Class<T> type) {
		com.g0dkar.leet.util.ognl.typeConversion.TypeConverter converter = TypeConverterManager.getConverterFor(type);
		
		try {
			return (T) converter.fromString(string, type, null);
		} catch (Exception e) {
			return null;
		}
	}
	
	public static Object getWithException(String field, Object object) throws OgnlException {
		return Ognl.getValue(field, OGNL_CONTEXT, object);
	}
	
	/**
	 * Builds a Object based on a attribute map. Every key on the map is a OGNL property whom will be se with the
	 * corresponding value.
	 * 
	 * @param klass
	 *            The Object {@link Class}
	 * @param values
	 *            The values map
	 * @return The built Object
	 */
	public static Object buildObject(Class<?> klass, Map<String, Object> values) {
		Object obj = ReflectionUtils.newInstance(klass);
		
		for (Entry<String, Object> value : values.entrySet()) {
			set(value.getKey(), value.getValue(), obj);
		}
		
		return obj;
	}
	
	public static void fillObject(Object object, Map<String, Object> values) {
		if (object != null) {
			for (Entry<String, Object> value : values.entrySet()) {
				set(value.getKey(), value.getValue(), object);
			}
		}
	}
	
	public static String asOGNLArray(Collection<?> elements, Class<?> elementType) {
		if (elements == null || elements.isEmpty()) {
			return "";
		}
		else {
			StringBuilder str = new StringBuilder("{");
			List<String> strings = new ArrayList<String>();
			
			if (elementType.equals(String.class)) {
				strings.addAll((Collection<String>) elements);
			}
			else {
				com.g0dkar.leet.util.ognl.typeConversion.TypeConverter typeConverter = TypeConverterManager.getConverterFor(elementType);
				
				if (typeConverter != null) {
					for (Object object : elements) {
						try {
							strings.add(typeConverter.toString(object, null));
						} catch (Exception e) {
							strings.add("");
						}
					}
				}
			}
			
			for (int i = 0, size = strings.size(); i < size; i++) {
				str.append("'");
				str.append(strings.get(i));
				str.append("'");
				
				if (i < size - 1) {
					str.append(", ");
				}
			}
			
			str.append("}");
			
			return str.toString();
		}
	}
}
